feat(share): zero-knowledge plan review for brainstorm flows - #38
Merged
Conversation
Establishes the shared contracts for the shared-review epic: Go types (PasteShare, PasteEvent, DTOs), Storage interface with sentinel errors, SQLite implementation with embed-based migrations, AES-256-GCM crypto helpers, and TypeScript mirrors with Vitest unit tests.
Implements five endpoints (POST, GET, PUT, DELETE, POST blobs) with Bearer token auth, proper HTTP status codes, and 10 test cases covering happy paths, auth errors, and missing resource cases.
Implement four utility modules for the paste/shared-review feature: client.ts (fetch wrappers), anchor.ts (4-step resilience resolution), events.ts (CRDT-lite replay), and identity.ts (localStorage name capture). All modules are fully tested with 16 passing Vitest unit tests.
Implements the SvelteKit route web/src/routes/share/[id]/ with all 11 components for the encrypted plan review UI: PlanRenderer (markdown with source-line anchors), AnnotationToolbar, LabelPicker, CommentCard, CommentThread, ResolveControls, SuggestionDiff, NamePromptModal, and DriftBadge. The route decrypts pastes via window.location.hash key, replays comment events, and gates submission on a reviewer name prompt.
Wire the paste package into arc-server so that /api/paste/* endpoints are available at startup. Paste migrations run automatically on the same SQLite DB as arc's main storage. The SPA fallback already handles /share/[id] via the existing embed_webui.go handler. - Add pastesqlite.Apply call in initSchema after arc migrations succeed - Add DB() *sql.DB accessor to sqlite.Store for use by route registration - Create paste_routes.go with registerPasteRoutes(e, db) helper - Pass DB to api.Config in server.go (conditional: no-op when nil) - Wire store.DB() into api.Config in internal/server/server.go - Add TestPasteRoutesMounted and TestShareRouteFallsBackToSPA tests
Implements `arc share create|list|show|comments|pull|approve|update|delete` with client-side AES-GCM encryption via the paste package and persists share metadata (key, edit_token) to ~/.arc/shares.json (mode 0600).
Replaces the initial review surface with an Editorial Marginalia aesthetic (dark paper, Newsreader serif body, ink-color metaphor) and a plannotator-style floating toolbar. The selection-driven workflow now offers six distinct intents (praise, comment, delete, suggest, quick-label, dismiss) instead of the original inline AnnotationToolbar. - Adds scoped --ink-* design tokens under .share-page in app.css - Skips arc's sidebar + projects fetch on /share/* via isShareRoute guard - Wraps quoted_text inline with <mark> via DOM walk (inline-annotations.ts) - Splits the right rail into AnnotationsPanel + AnnotationCard - Pulls comment/suggest into a single CommentPopover - Adds QuickLabelPicker for ⚡ menu with 1..5 keyboard shortcuts - Renders floating overlays inside .share-page so they inherit ink tokens - Adds action: 'comment' | 'delete' field to CommentEvent - Adds @tailwindcss/typography devDep Removes superseded components: AnnotationToolbar, CommentCard, CommentThread, DriftBadge, LabelPicker, ResolveControls, SuggestionDiff. Threading, suggestion diff rendering, and drift badges are deferred (see status doc).
Three small, low-risk cleanups bundled together: - Scope vitest's include glob to src/lib/paste/** so it doesn't pick up legacy bun:test files elsewhere in web/. - Replace `as any` gymnastics in paste/crypto.ts with a single asArrayBuffer() helper. Functionally identical, just clearer. - Run `bun run format` + lint:fix across web/. No behavioral changes — only trailing commas, line breaks, and quoting normalized by Biome/Prettier. The format pass touches a lot of files that look unrelated; verifying with `git diff --stat` shows no functional edits outside the paste lib.
Without the webui build tag, arc-paste's embedded SPA assets are zero bytes and GET /share/<id> returns a JSON 404 from the API fallback instead of serving the SvelteKit app shell. Mirrors the flag the main `build` target already passes.
Step-by-step manual verification flow for the share/[id] surface, covering local mode (arc share create --local) and the deferred public arc-paste deploy. Codifies what was previously tribal knowledge from manual SSH-tunnel testing.
CLI - Capture author identity at create time via new resolveAuthor helper: --author flag > share_author in cli-config.json > $ARC_SHARE_AUTHOR > `git config user.name`. Without an author, plan.author_name stays empty and the share UI's Accept/Resolve/Reject controls never appear — warn loudly when this happens. - New share_server config field with the same precedence (flag > config > env > built-in default). Default URL is now arcplanner.sentiolabs.io. - Wire up `--title` flag with filename-stem fallback so the SPA header shows a real title instead of "Untitled plan". - printComments now applies edit events, mirroring the SPA's replay so `arc share comments` / `arc share pull` see refined wording, not stale. Web - Replace asymmetric-inset doc layout with mx-auto centering: equal gaps between the viewport edge → doc and doc → rail seam at any width. - Add reviewer self-edit: new kind="edit" event references a comment by id and merges body / suggested_text / comment_type at replay time. Author gate (edit.author_name === comment.author_name) lives in the replay layer since the encrypted server can't enforce it. Inline edit UI in AnnotationCard appears only on the reviewer's own open comments; ⌘/Ctrl-⏎ saves, esc cancels. Card shows "edited Nm" indicator. - Clamp floating overlays (CommentPopover, QuickLabelPicker, FloatingToolbar) to viewport bounds so they don't clip off-screen when the selection is near a margin. - Detect macOS via userAgentData.platform → navigator.platform fallback; show ⌘ on Mac, Ctrl elsewhere in keyboard hints. Tests - 7 new replay-event tests (edit application, forged-edit rejection, multiple edits, keep-vs-clear semantics, orphan handling). - 6 positioning-clamp tests, 8 platform-detection tests. - TestResolveAuthor and TestResolveServer lock the precedence chains. - TestRunShareCommentsAppliesEdits verifies CLI parity with SPA replay including a Mallory-forges-Steve's-edit case. Docs - New docs/review_howto.md explaining Accept/Resolve/Reject semantics for plan authors, oriented around the LLM-consumer workflow. - Runbook updated with the new author/server config precedence and the reviewer self-edit step.
Dockerfile rewritten as a 3-stage build:
- Stage 1: bun:1-alpine builds the SvelteKit SPA (matches local toolchain).
- Stage 2: golang:1.26-alpine compiles a static binary with `-tags webui`,
`-trimpath`, and `-ldflags='-s -w'` for a smaller, reproducible artifact.
The previous Dockerfile was missing `-tags webui`, which would have
produced a binary where /share/<id> returned Echo's default JSON 404
because web.RegisterSPA was the no-op stub.
- Stage 3: scratch — just the binary, ~10 MB final image.
arc-paste/main.go now MkdirAll's the SQLite parent dir on startup, so
fresh volumes / nested DB paths work without an init step. This matters
on scratch where the binary can't rely on a base image creating /data,
and helps any deploy that points ARC_PASTE_DB at a path the host hasn't
pre-created.
arc-paste/compose.yaml ships alongside, with a named volume for SQLite
persistence, port 7433, restart: unless-stopped, and a comment explaining
why no in-container healthcheck (scratch has no shell). Build with:
docker compose -f arc-paste/compose.yaml up -d --build
Runbook updated with the Docker option in the remote-review section.
Two related improvements to the reviewer→LLM pipeline:
1. arc share comments --json now emits a single JSON object designed
for direct LLM consumption:
{
"plan": { id, title, author_name,
file (when local) | markdown_b64 (when not) },
"comments": [{ comment, status, reply,
resolved_anchor: { status, line_start, line_end,
snippet } }]
}
- The `file` ⊕ `markdown_b64` split avoids dragging the plan content
through every CLI call when the agent already has it on disk.
Base64 sidesteps JSON's escape penalty (every \n and \" doubles
the byte count and destroys readability) when content IS shipped.
- `comment.action` is now preserved on the Go side so delete
annotations round-trip correctly (was silently dropped before).
- `resolved_anchor` runs the SPA's 4-step anchor fallback (exact →
heading → fuzzy → orphaned) against the live plan content, so
line numbers are ground-truth for what the agent will edit.
- New internal/paste/anchor.go ports anchor.ts to Go; 9 tests cover
every fallback path plus a TS-compatibility check on Slugify.
2. Plan author can now edit any reviewer's annotation, not just their
own. The displayed comment.author_name doesn't change — only the
body/suggested_text/comment_type — so attribution is preserved and
the audit trail (in the underlying edit event) records who actually
edited. This lets the author sharpen thin "expand this more"
feedback into something LLM-actionable without round-trips.
Replay-time auth gate updated in both events.ts and share.go:
accepts edits where author_name matches the comment's original
author OR the plan's author (with non-empty planAuthor guard so
empty-string-matches-empty-string doesn't grant rights).
Tests
- TestRunShareCommentsJSONBundle: bundle shape, action round-trip,
resolved_anchor against the on-disk file, snippet content.
- TestRunShareCommentsJSONBundle_NoLocalFile: simulates a cross-machine
agent (wipes shares.json mid-test) and verifies markdown_b64 round-
trips with newlines + quotes preserved.
- TestRunShareCommentsAppliesPlanAuthorEdits: Steve's body becomes
Ben's wording; Mallory's forged edit dropped; Steve still attributed.
- 4 new sub-tests in events.test.ts mirror the auth corners (plan-
author edit, third-party rejection, empty-author trap, chronological
ordering across both roles).
- 9 anchor_test.go tests including out-of-range fall-through and
TS-compatible slugify.
Docs
- review_howto.md: full JSON bundle schema with the two cases (local
/ remote) and field-by-field guide for agent consumers. New
"Editing annotations" section explains the two-role edit model.
- runbooks/paste-server.md: step 8 added — manual test of author
editing a reviewer's comment.
… toolbar Three intertwined improvements to the /share/[id] surface: 1. Fix the broken plan renderer The previous PlanRenderer split markdown on blank lines to attach per-block `data-source-line` attributes for selection anchoring. That destroyed fenced code blocks containing blank lines: the SQL schema, the TypeScript type definitions, and the `arc share *` CLI block all rendered as flowing prose with `# comment` lines becoming <h1> headings and the rest of the fence body becoming <p>. Fixed by switching to `marked.lexer()` to enumerate top-level tokens (each fenced block is ONE token, blank lines and all). Per-token line numbers come from `token.raw`'s offset in the source. The full document renders once through the project's existing shiki-powered `renderMarkdown` (github-dark-dimmed theme) for syntax highlighting, then DOMParser walks the result and tags each top-level element with data-source-line directly — no wrapper divs. 2. Match /planner's color theme The share page's editorial Newsreader/warm-amber palette read as a visual outlier inside arc. Now `--ink-paper`, `--ink-text`, `--ink-rule` etc. alias to arc's `--color-surface-*` / `--color-text-*` / `--color-border` tokens, the doc inherits Instrument Sans, headings drop the italic display style, bullets become standard discs, and inline code uses the planner's indigo- on-raised-surface treatment. Annotation-domain tokens (`--ink-comment` amber highlight, `--ink-delete` red, `--ink-praise` green) stay distinct since they encode review semantics, not chrome. `--ink-delete` and `--ink-praise` are now derived from `--color-status-blocked` and `--color-status-open` via `oklch(from var(...) l c h / alpha)` so any future palette tuning carries through automatically. 3. Merge the two action rows in AnnotationCard When the plan author viewed an editable comment, they saw two separate bordered toolbars: one for ✎ Edit, another for Accept/Resolve/Reject. Single-row treatment with a typographic `·` separator (the same glyph used elsewhere in the card for metadata) reads as one decision row instead of two stacked choices. Color hierarchy already differentiates the soft action (Edit, muted text) from the load-bearing decisions (Accept green, Reject red), so no extra styling needed. Tests 44 web tests still pass; svelte-check clean. No test changes — these are visual / rendering-pipeline fixes covered by the existing replay + components test suite.
…screens
The doc was hard-capped at max-w-[760px] regardless of viewport, so
1200px and 1920px monitors had identical doc widths with the latter
showing ~400px of empty space between the doc and the rail.
Switch to a single clamp() that interpolates smoothly between three
principled bounds:
floor 47.5rem (760px) — current width; viewports that already feel
comfortable don't change behavior.
pref 80% — 80% of the doc-area (= 1fr column = vw - 360
rail). Grows fluidly with available space.
ceil 60rem (960px) — readable upper bound. Past ~95ch the eye's
saccade between line-end and next-line gets
too long for comfortable prose. Code blocks
tolerate the extra width fine.
Effective widths by viewport:
1100px (doc-area 740): 740 (parent caps) — same as before
1200px (doc-area 840): 760 (floor wins) — same as before
1440px (doc-area 1080): 864 (80% pref) — +104
1600px (doc-area 1240): 960 (ceiling) — +200
1920px+: 960 (ceiling) — +200, stable
Static `max-w-[760px]` removed from the markup so the CSS clamp is
the single source of truth — no Tailwind/CSS conflict.
Side benefit: ASCII-art code blocks (e.g. the architecture diagram in
the shared-review plan) fit at 960px without horizontal scroll. The
narrow-viewport reading experience is unchanged.
…ages Brings the repo back to a clean lint state after the share/paste feature landed. Highlights: - Rename paste.PasteShare/PasteEvent → paste.Share/Event (revive exported rule) and update all call sites. - Hoist magic numbers to named constants (share ID length, edit token bytes, heading window, file modes, etc.). - Replace deprecated middleware.Logger with RequestLoggerWithConfig in arc-paste; restructure main into run() error so deferred db.Close runs. - Decompose printComments (cognitive complexity 52 → small helpers), resolveShareRef nesting, and TestResolveServer table-driven. - Move paste tests to package paste_test; use t.Setenv; tighten test file perms to 0o600; add //nolint:gosec with reasons where the variable URL is the deliberate CLI behaviour. - Add ErrShareNotFound to sharesconfig (replacing nil/nil return) and thread errors.Is checks through callers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an end-to-end zero-knowledge plan-review surface that lives at
/share/[id]. The author runsarc share create <plan.md>, copies the URL (with the decryption key in the fragment), shares it with reviewers; reviewers annotate inline; the author accepts/resolves/rejects; the agent pulls accepted comments back viaarc share pull <id> --jsonand applies the changes.The feature is built in two layers:
cfcbce9→a9beb46): foundation. Encrypted paste API ininternal/paste/, optional standalonearc-pastebinary, CLI subcommands (arc share create | list | comments | pull | approve | update | delete), SvelteKit SPA at/share/[id]with selection-anchored annotations, runbook.649d47e→0f8c129): polish and agent-pipeline. Configurable author/server identity, reviewer + plan-author annotation editing, planner-aligned theme, fluid doc width, fixed markdown rendering, viewport-clamped overlays, platform-aware keyboard hints, scratch-based Docker deployment, and a single-object JSON bundle output designed for direct LLM consumption.What ships
Server-side (Go)
internal/paste/package with HTTP handlers, opaque blob storage, AES-256-GCM crypto helpers, and a parallel anchor-resolution implementation that mirrors the SPA's 4-step fallback (exact → heading-scoped → fuzzy → orphaned).arc-paste/standalone binary for public deployments separate from the main arc-server.CLI (Go)
arc share *subcommands wired through~/.arc/shares.jsonregistry that holds the URL + edit_token + key per share.share_authorandshare_serverconfig fields with full precedence chains (flag → config → env → fallback). Forshare_authorthe fallback chain ends atgit config user.name; forshare_serverit ends at the built-inarcplanner.sentiolabs.io.arc share comments --jsonemits a single bundle object:{plan: {id, title, file ⊕ markdown_b64}, comments: [{comment, status, reply, resolved_anchor: {status, line_start, line_end, snippet}}]}. Thefile⊕markdown_b64xor lets the agent infer source: file path when local, base64-encoded markdown when not. Designed forarc share pull <id> | <agent>.Web SPA (Svelte 5)
/share/[id]route with a focused review surface (the standard arc sidebar is hidden via anisShareRouteguard so the route also works on a public-paste deploy with no projects API).author_name.clamp(47.5rem, 80%, 60rem)so wide-screen viewports get breathing room without sacrificing the narrow-screen reading experience./planner(Instrument Sans, surface-900 surfaces, indigo accents). Annotation-domain colors (amber comment, red delete, sage praise) preserved as semantic markers.marked.lexer()-based source-line tracking + the project's existing shiki pipeline (github-dark-dimmedtheme). The previous implementation broke fenced code blocks containing blank lines.Build / ops
arc-paste/Dockerfilerewritten as a 3-stage build withbun:1-alpine+golang:1.26-alpine+scratchruntime,-tags webui(was missing — would have shipped a binary that returned JSON 404 for/share/<id>),-trimpathand-ldflags='-s -w'. ~10MB final image.arc-paste/compose.yamlwith named volume for SQLite persistence,restart: unless-stopped, port 7433.Documentation
docs/runbooks/paste-server.md— manual test runbook covering local + remote modes, Docker option, troubleshooting table.docs/review_howto.md— Accept/Resolve/Reject semantics for plan authors, oriented around the LLM-consumer goal, with the JSON bundle schema documented.Test coverage
Test plan
make build— confirmbin/arcandbin/arc-pasteproduced withwebuitag (smoke check:curl http://localhost:7432/share/fooshould return SPA HTML, not JSON 404).cd web && bun run check && bun run test— should be 0 errors / 0 warnings / all tests passing.go test ./...— all packages green.docs/runbooks/paste-server.md):./bin/arc server start --foreground, run./bin/arc share create /tmp/p.md --local, open URL.<mark>appears inline + card in right rail.· edited Nmindicator.author_namestays as the reviewer../bin/arc share comments <id> --json | jq .— verify bundle shape withplan.file,plan.markdown_b64absent,resolved_anchor.status: "ok", etc../bin/arc share pull <id>— verify only accepted comments print.docker compose -f arc-paste/compose.yaml up -d --build, smoke a remote share againsthttp://localhost:7433.Ctrl ⏎shows on Linux/Windows,⌘ ⏎on macOS.